﻿// FIXME: Update this file to be null safe and then delete the line below
#nullable disable

using System.Net;
using Bit.Admin.AdminConsole.Models;
using Bit.Admin.Enums;
using Bit.Admin.Services;
using Bit.Admin.Utilities;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Enums;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Core.Vault.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Bit.Admin.AdminConsole.Controllers;

[Authorize]
public class OrganizationsController : Controller
{
    private readonly IOrganizationRepository _organizationRepository;
    private readonly IOrganizationUserRepository _organizationUserRepository;
    private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
    private readonly ISelfHostedSyncSponsorshipsCommand _syncSponsorshipsCommand;
    private readonly ICipherRepository _cipherRepository;
    private readonly ICollectionRepository _collectionRepository;
    private readonly IGroupRepository _groupRepository;
    private readonly IPolicyRepository _policyRepository;
    private readonly IPaymentService _paymentService;
    private readonly IApplicationCacheService _applicationCacheService;
    private readonly GlobalSettings _globalSettings;
    private readonly IProviderRepository _providerRepository;
    private readonly ILogger<OrganizationsController> _logger;
    private readonly IAccessControlService _accessControlService;
    private readonly ISecretRepository _secretRepository;
    private readonly IProjectRepository _projectRepository;
    private readonly IServiceAccountRepository _serviceAccountRepository;
    private readonly IProviderOrganizationRepository _providerOrganizationRepository;
    private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
    private readonly IProviderBillingService _providerBillingService;
    private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand;
    private readonly IPricingClient _pricingClient;
    private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;

    public OrganizationsController(
        IOrganizationRepository organizationRepository,
        IOrganizationUserRepository organizationUserRepository,
        IOrganizationConnectionRepository organizationConnectionRepository,
        ISelfHostedSyncSponsorshipsCommand syncSponsorshipsCommand,
        ICipherRepository cipherRepository,
        ICollectionRepository collectionRepository,
        IGroupRepository groupRepository,
        IPolicyRepository policyRepository,
        IPaymentService paymentService,
        IApplicationCacheService applicationCacheService,
        GlobalSettings globalSettings,
        IProviderRepository providerRepository,
        ILogger<OrganizationsController> logger,
        IAccessControlService accessControlService,
        ISecretRepository secretRepository,
        IProjectRepository projectRepository,
        IServiceAccountRepository serviceAccountRepository,
        IProviderOrganizationRepository providerOrganizationRepository,
        IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
        IProviderBillingService providerBillingService,
        IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand,
        IPricingClient pricingClient,
        IResendOrganizationInviteCommand resendOrganizationInviteCommand)
    {
        _organizationRepository = organizationRepository;
        _organizationUserRepository = organizationUserRepository;
        _organizationConnectionRepository = organizationConnectionRepository;
        _syncSponsorshipsCommand = syncSponsorshipsCommand;
        _cipherRepository = cipherRepository;
        _collectionRepository = collectionRepository;
        _groupRepository = groupRepository;
        _policyRepository = policyRepository;
        _paymentService = paymentService;
        _applicationCacheService = applicationCacheService;
        _globalSettings = globalSettings;
        _providerRepository = providerRepository;
        _logger = logger;
        _accessControlService = accessControlService;
        _secretRepository = secretRepository;
        _projectRepository = projectRepository;
        _serviceAccountRepository = serviceAccountRepository;
        _providerOrganizationRepository = providerOrganizationRepository;
        _removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
        _providerBillingService = providerBillingService;
        _organizationInitiateDeleteCommand = organizationInitiateDeleteCommand;
        _pricingClient = pricingClient;
        _resendOrganizationInviteCommand = resendOrganizationInviteCommand;
    }

    [RequirePermission(Permission.Org_List_View)]
    public async Task<IActionResult> Index(string name = null, string userEmail = null, bool? paid = null,
        int page = 1, int count = 25)
    {
        if (page < 1)
        {
            page = 1;
        }

        if (count < 1)
        {
            count = 1;
        }

        var encodedName = WebUtility.HtmlEncode(name);
        var skip = (page - 1) * count;
        var organizations = await _organizationRepository.SearchAsync(encodedName, userEmail, paid, skip, count);
        return View(new OrganizationsModel
        {
            Items = organizations as List<Organization>,
            Name = string.IsNullOrWhiteSpace(name) ? null : name,
            UserEmail = string.IsNullOrWhiteSpace(userEmail) ? null : userEmail,
            Paid = paid,
            Page = page,
            Count = count,
            Action = _globalSettings.SelfHosted ? "View" : "Edit",
            SelfHosted = _globalSettings.SelfHosted
        });
    }

    public async Task<IActionResult> View(Guid id)
    {
        var organization = await _organizationRepository.GetByIdAsync(id);
        if (organization == null)
        {
            return RedirectToAction("Index");
        }

        var provider = await _providerRepository.GetByOrganizationIdAsync(id);
        var ciphers = await _cipherRepository.GetManyByOrganizationIdAsync(id);
        var collections = await _collectionRepository.GetManyByOrganizationIdAsync(id);
        IEnumerable<Group> groups = null;
        if (organization.UseGroups)
        {
            groups = await _groupRepository.GetManyByOrganizationIdAsync(id);
        }
        IEnumerable<Policy> policies = null;
        if (organization.UsePolicies)
        {
            policies = await _policyRepository.GetManyByOrganizationIdAsync(id);
        }
        var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id);
        var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null;
        var secrets = organization.UseSecretsManager ? await _secretRepository.GetSecretsCountByOrganizationIdAsync(id) : -1;
        var projects = organization.UseSecretsManager ? await _projectRepository.GetProjectCountByOrganizationIdAsync(id) : -1;
        var serviceAccounts = organization.UseSecretsManager ? await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(id) : -1;
        var smSeats = organization.UseSecretsManager
            ? await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
            : -1;
        return View(new OrganizationViewModel(organization, provider, billingSyncConnection, users, ciphers, collections, groups, policies,
            secrets, projects, serviceAccounts, smSeats));
    }

    [SelfHosted(NotSelfHostedOnly = true)]
    public async Task<IActionResult> Edit(Guid id)
    {
        var organization = await _organizationRepository.GetByIdAsync(id);
        if (organization == null)
        {
            return RedirectToAction("Index");
        }

        var provider = await _providerRepository.GetByOrganizationIdAsync(id);
        var ciphers = await _cipherRepository.GetManyByOrganizationIdAsync(id);
        var collections = await _collectionRepository.GetManyByOrganizationIdAsync(id);
        IEnumerable<Group> groups = null;
        if (organization.UseGroups)
        {
            groups = await _groupRepository.GetManyByOrganizationIdAsync(id);
        }
        IEnumerable<Policy> policies = null;
        if (organization.UsePolicies)
        {
            policies = await _policyRepository.GetManyByOrganizationIdAsync(id);
        }
        var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id);
        var billingInfo = await _paymentService.GetBillingAsync(organization);
        var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(organization);
        var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null;
        var secrets = organization.UseSecretsManager ? await _secretRepository.GetSecretsCountByOrganizationIdAsync(id) : -1;
        var projects = organization.UseSecretsManager ? await _projectRepository.GetProjectCountByOrganizationIdAsync(id) : -1;
        var serviceAccounts = organization.UseSecretsManager ? await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(id) : -1;

        var smSeats = organization.UseSecretsManager
            ? await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
            : -1;

        var plans = await _pricingClient.ListPlans();

        return View(new OrganizationEditModel(
            organization,
            provider,
            users,
            ciphers,
            collections,
            groups,
            policies,
            billingInfo,
            billingHistoryInfo,
            billingSyncConnection,
            _globalSettings,
            plans,
            secrets,
            projects,
            serviceAccounts,
            smSeats));
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    [SelfHosted(NotSelfHostedOnly = true)]
    public async Task<IActionResult> Edit(Guid id, OrganizationEditModel model)
    {
        var organization = await _organizationRepository.GetByIdAsync(id);

        if (organization == null)
        {
            TempData["Error"] = "Could not find organization to update.";
            return RedirectToAction("Index");
        }

        var existingOrganizationData = new Organization
        {
            Id = organization.Id,
            Status = organization.Status,
            PlanType = organization.PlanType,
            Seats = organization.Seats
        };

        if (model.PlanType.HasValue)
        {
            var freePlan = await _pricingClient.GetPlanOrThrow(model.PlanType.Value);
            var isDowngradingToFree = organization.PlanType != PlanType.Free && model.PlanType.Value == PlanType.Free;
            if (isDowngradingToFree)
            {
                if (model.Seats.HasValue && model.Seats.Value > freePlan.PasswordManager.MaxSeats)
                {
                    TempData["Error"] = $"Organizations with more than {freePlan.PasswordManager.MaxSeats} seats cannot be downgraded to the Free plan";
                    return RedirectToAction("Edit", new { id });
                }

                if (model.MaxCollections > freePlan.PasswordManager.MaxCollections)
                {
                    TempData["Error"] = $"Organizations with more than {freePlan.PasswordManager.MaxCollections} collections cannot be downgraded to the Free plan. Your organization currently has {organization.MaxCollections} collections.";
                    return RedirectToAction("Edit", new { id });
                }

                model.MaxStorageGb = null;
                model.ExpirationDate = null;
                model.Enabled = true;
            }
        }

        UpdateOrganization(organization, model);
        var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
        if (organization.UseSecretsManager && !plan.SupportsSecretsManager)
        {
            TempData["Error"] = "Plan does not support Secrets Manager";
            return RedirectToAction("Edit", new { id });
        }

        await HandlePotentialProviderSeatScalingAsync(
            existingOrganizationData,
            model);

        await _organizationRepository.ReplaceAsync(organization);

        await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);

        return RedirectToAction("Edit", new { id });
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    [RequirePermission(Permission.Org_Delete)]
    public async Task<IActionResult> Delete(Guid id)
    {
        var organization = await _organizationRepository.GetByIdAsync(id);

        if (organization == null)
        {
            return RedirectToAction("Index");
        }

        if (organization.IsValidClient())
        {
            var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);

            if (provider.IsBillable())
            {
                await _providerBillingService.ScaleSeats(
                    provider,
                    organization.PlanType,
                    -organization.Seats ?? 0);
            }
        }

        await _organizationRepository.DeleteAsync(organization);
        await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);

        return RedirectToAction("Index");
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    [RequirePermission(Permission.Org_RequestDelete)]
    public async Task<IActionResult> DeleteInitiation(Guid id, OrganizationInitiateDeleteModel model)
    {
        if (!ModelState.IsValid)
        {
            TempData["Error"] = ModelState.GetErrorMessage();
        }
        else
        {
            try
            {
                var organization = await _organizationRepository.GetByIdAsync(id);
                if (organization != null)
                {
                    await _organizationInitiateDeleteCommand.InitiateDeleteAsync(organization, model.AdminEmail);
                    TempData["Success"] = "The request to initiate deletion of the organization has been sent.";
                }
            }
            catch (Exception ex)
            {
                TempData["Error"] = ex.Message;
            }
        }

        return RedirectToAction("Edit", new { id });
    }

    public async Task<IActionResult> TriggerBillingSync(Guid id)
    {
        var organization = await _organizationRepository.GetByIdAsync(id);
        if (organization == null)
        {
            return RedirectToAction("Index");
        }
        var connection = (await _organizationConnectionRepository.GetEnabledByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync)).FirstOrDefault();
        if (connection != null)
        {
            try
            {
                var config = connection.GetConfig<BillingSyncConfig>();
                await _syncSponsorshipsCommand.SyncOrganization(id, config.CloudOrganizationId, connection);
                TempData["ConnectionActivated"] = id;
                TempData["ConnectionError"] = null;
            }
            catch (Exception ex)
            {
                TempData["ConnectionError"] = ex.Message;
                _logger.LogWarning(ex, "Error while attempting to do billing sync for organization with id '{OrganizationId}'", id);
            }

            if (_globalSettings.SelfHosted)
            {
                return RedirectToAction("View", new { id });
            }
            else
            {
                return RedirectToAction("Edit", new { id });
            }
        }
        return RedirectToAction("Index");
    }

    [HttpPost]
    public async Task<IActionResult> ResendOwnerInvite(Guid id)
    {
        var organization = await _organizationRepository.GetByIdAsync(id);
        if (organization == null)
        {
            return RedirectToAction("Index");
        }

        var organizationUsers = await _organizationUserRepository.GetManyByOrganizationAsync(id, OrganizationUserType.Owner);
        foreach (var organizationUser in organizationUsers)
        {
            await _resendOrganizationInviteCommand.ResendInviteAsync(id, null, organizationUser.Id, true);
        }

        return Json(null);
    }

    [HttpPost]
    [RequirePermission(Permission.Provider_Edit)]
    public async Task<IActionResult> UnlinkOrganizationFromProviderAsync(Guid id)
    {
        var organization = await _organizationRepository.GetByIdAsync(id);
        if (organization is null)
        {
            return RedirectToAction("Index");
        }

        var provider = await _providerRepository.GetByOrganizationIdAsync(id);
        if (provider is null)
        {
            return RedirectToAction("Edit", new { id });
        }

        var providerOrganization = await _providerOrganizationRepository.GetByOrganizationId(id);
        if (providerOrganization is null)
        {
            return RedirectToAction("Edit", new { id });
        }

        await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(
            provider,
            providerOrganization,
            organization);

        return Json(null);
    }

    private void UpdateOrganization(Organization organization, OrganizationEditModel model)
    {
        if (_accessControlService.UserHasPermission(Permission.Org_Name_Edit))
        {
            organization.Name = WebUtility.HtmlEncode(model.Name);
        }

        if (_accessControlService.UserHasPermission(Permission.Org_CheckEnabledBox))
        {
            organization.Enabled = model.Enabled;
        }

        if (_accessControlService.UserHasPermission(Permission.Org_Plan_Edit))
        {
            organization.PlanType = model.PlanType.Value;
            organization.Plan = model.Plan;
            organization.Seats = model.Seats;
            organization.MaxAutoscaleSeats = model.MaxAutoscaleSeats;
            organization.MaxCollections = model.MaxCollections;
            organization.MaxStorageGb = model.MaxStorageGb;

            //features
            organization.SelfHost = model.SelfHost;
            organization.Use2fa = model.Use2fa;
            organization.UseApi = model.UseApi;
            organization.UseGroups = model.UseGroups;
            organization.UsePolicies = model.UsePolicies;
            organization.UseSso = model.UseSso;
            organization.UseKeyConnector = model.UseKeyConnector;
            organization.UseScim = model.UseScim;
            organization.UseDirectory = model.UseDirectory;
            organization.UseEvents = model.UseEvents;
            organization.UseResetPassword = model.UseResetPassword;
            organization.UseCustomPermissions = model.UseCustomPermissions;
            organization.UseTotp = model.UseTotp;
            organization.UsersGetPremium = model.UsersGetPremium;
            organization.UseSecretsManager = model.UseSecretsManager;
            organization.UseRiskInsights = model.UseRiskInsights;
            organization.UseOrganizationDomains = model.UseOrganizationDomains;
            organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;

            //secrets
            organization.SmSeats = model.SmSeats;
            organization.MaxAutoscaleSmSeats = model.MaxAutoscaleSmSeats;
            organization.SmServiceAccounts = model.SmServiceAccounts;
            organization.MaxAutoscaleSmServiceAccounts = model.MaxAutoscaleSmServiceAccounts;
        }

        if (_accessControlService.UserHasPermission(Permission.Org_Licensing_Edit))
        {
            organization.LicenseKey = model.LicenseKey;
            organization.ExpirationDate = model.ExpirationDate;
        }

        if (_accessControlService.UserHasPermission(Permission.Org_Billing_Edit))
        {
            organization.BillingEmail = model.BillingEmail?.ToLowerInvariant()?.Trim();
            organization.Gateway = model.Gateway;
            organization.GatewayCustomerId = model.GatewayCustomerId;
            organization.GatewaySubscriptionId = model.GatewaySubscriptionId;
        }
    }

    private async Task HandlePotentialProviderSeatScalingAsync(
        Organization organization,
        OrganizationEditModel update)
    {
        var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);

        // No scaling required
        if (provider is not { Type: ProviderType.Msp, Status: ProviderStatusType.Billable } ||
            organization is not { Status: OrganizationStatusType.Managed } ||
            !organization.Seats.HasValue ||
            update is { Seats: null, PlanType: null } ||
            update is { PlanType: not PlanType.TeamsMonthly and not PlanType.EnterpriseMonthly } ||
            (PlanTypesMatch() && SeatsMatch()))
        {
            return;
        }

        // Only scale the plan
        if (!PlanTypesMatch() && SeatsMatch())
        {
            await _providerBillingService.ScaleSeats(provider, organization.PlanType, -organization.Seats.Value);
            await _providerBillingService.ScaleSeats(provider, update.PlanType!.Value, organization.Seats.Value);
        }
        // Only scale the seats
        else if (PlanTypesMatch() && !SeatsMatch())
        {
            var seatAdjustment = update.Seats!.Value - organization.Seats.Value;
            await _providerBillingService.ScaleSeats(provider, organization.PlanType, seatAdjustment);
        }
        // Scale both
        else if (!PlanTypesMatch() && !SeatsMatch())
        {
            var seatAdjustment = update.Seats!.Value - organization.Seats.Value;
            var planTypeAdjustment = organization.Seats.Value;
            var totalAdjustment = seatAdjustment + planTypeAdjustment;

            await _providerBillingService.ScaleSeats(provider, organization.PlanType, -organization.Seats.Value);
            await _providerBillingService.ScaleSeats(provider, update.PlanType!.Value, totalAdjustment);
        }

        return;

        bool PlanTypesMatch()
            => update.PlanType.HasValue && update.PlanType.Value == organization.PlanType;

        bool SeatsMatch()
            => update.Seats.HasValue && update.Seats.Value == organization.Seats;
    }
}
